﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace gheat
{
    /// <summary>
    /// Thrown when no weight handler is set and some function is called requiring a weight handler
    /// </summary>
    public class NoWeightHandler : Exception 
    {
        public NoWeightHandler(String message) : base(message) {}
    }

    /// <summary>
    /// Manages the points to be applied to the tiles
    /// </summary>
    public class PointManager
    {
        List<GMap.NET.PointLatLng> _pointList;
        private GMap.NET.Projections.MercatorProjection _projection = new GMap.NET.Projections.MercatorProjection();
        IWeightHandler _weightHandler;

        public PointManager():this(null){}

        /// <summary>
        /// Weight handle is used to get the weight for each point
        /// </summary>
        /// <param name="weightHandle"></param>
        public PointManager(IWeightHandler weightHandle)
        {
            _pointList = new List<GMap.NET.PointLatLng>();
            _weightHandler = weightHandle;
        }

        /// <summary>
        /// Adds the point to the list. Also applies the weight to the point when their is point data
        /// </summary>
        /// <param name="point"></param>
        public void AddPoint(GMap.NET.PointLatLng point)
        {
            //Apply the weight to the new point
            if (_weightHandler != null && point.Data != null) point.Weight = _weightHandler.Evaluate(point.Data);
            _pointList.Add(point);
        }

        /// <summary>
        /// Adds the points to the list. Also applies the weight to the point when their is point data
        /// </summary>
        /// <param name="points"></param>
        public void AddPoint(GMap.NET.PointLatLng[] points)
        {
            //Apply the weight to the new point
            if (_weightHandler != null)
                for (int i = 0; i < points.Length;i++ )
                    if (points[i].Data != null) points[i].Weight = _weightHandler.Evaluate(points[i].Data);

            _pointList.AddRange(points) ;
        }

        /// <summary>
        /// Updates all of the weights for each point. Should not be called if their is no weight handlers
        /// </summary>
        public void UpdatePointWeights()
        {
            GMap.NET.PointLatLng tempPoint;
            if (_weightHandler == null)
                throw new NoWeightHandler("Point weights can't be updated because a weight handler was not specified");
            else
                for (int i = 0; i < _pointList.Count; i++)
                {
                    tempPoint = _pointList[i];
                    if (tempPoint.Data != null) tempPoint.Weight = _weightHandler.Evaluate(tempPoint.Data);
                    _pointList[i] = tempPoint;
                }
        }

        /// <summary>
        /// Gets the count of all of the points
        /// </summary>
        /// <returns></returns>
        public int PointCount()
        {
            return _pointList.Count;
        }

        /// <summary>
        /// Clears all of the points from the list
        /// </summary>
        public void ClearPointList()
        {
            _pointList.Clear(); 
        }

        /// <summary>
        /// Loads points from a file, just like in the Python GHeat app
        /// </summary>
        /// <param name="source"></param>
        /// <param name="weight">Force a weight on the point for testing</param>
        public void LoadPointsFromFile(string source,decimal weight)
        {
            string[] item;
            string[] lines = System.IO.File.ReadAllLines(source);
            foreach (string line in lines)
            {
                item = line.Split(',');
                _pointList.Add(new GMap.NET.PointLatLng(double.Parse(item[1]), double.Parse(item[2]),null,weight));
            }
        }

        /// <summary>
        /// Loads points from a file, just like in the Python GHeat app
        /// </summary>
        /// <param name="source"></param>
        public void LoadPointsFromFile(string source)
        {
            string[] item;
            string[] lines = System.IO.File.ReadAllLines(source);
            foreach (string line in lines)
            {
                item = line.Split(',');
                _pointList.Add(new GMap.NET.PointLatLng(double.Parse(item[1]), double.Parse(item[2])));
            }
        }

        /// <summary>
        /// Gets all of the points in the diameter specified
        /// </summary>
        /// <param name="center"></param>
        /// <param name="pixelsFromCenter">diameter</param>
        /// <param name="zoom">current zoom</param>
        /// <returns></returns>
        public GMap.NET.Point[] GetPointsAroundCenter(GMap.NET.PointLatLng center,int pixelsFromCenter, int zoom)
        {
            GMap.NET.Point centerAsPixels;
            GMap.NET.Point tlb;
            GMap.NET.Point lrb;
            List<GMap.NET.Point> points = new List<GMap.NET.Point>();
            GMap.NET.Size max_lrb;
            GMap.NET.Size min_tlb;
            GMap.NET.Point tempPoint;


            centerAsPixels = _projection.FromLatLngToPixel(center,zoom) ;
            min_tlb = _projection.GetTileMatrixMinXY(zoom);
            max_lrb = _projection.GetTileMatrixMaxXY(zoom);

            tlb = new GMap.NET.Point(
                             centerAsPixels.X - pixelsFromCenter,
                             centerAsPixels.Y - pixelsFromCenter);
            
            lrb = new GMap.NET.Point(
                           centerAsPixels.X + pixelsFromCenter,
                           centerAsPixels.Y + pixelsFromCenter);

            foreach (GMap.NET.PointLatLng llPoint in GetList(tlb, lrb, zoom, false))
            {
                //Add the point to the list
                tempPoint = _projection.FromLatLngToPixel(llPoint.Lat, llPoint.Lng, zoom);
                tempPoint.Data = llPoint.Data;
                tempPoint.Weight = llPoint.Weight;
                points.Add(tempPoint);
            }
            return points.ToArray();
        }

        /// <summary>
        /// Gets all of the points that fit on the tile
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="dot"></param>
        /// <param name="zoom"></param>
        /// <param name="newMethod"></param>
        /// <returns></returns>
        public GMap.NET.Point[]  GetPointsForTile(int x, int y,System.Drawing.Bitmap dot ,int zoom, bool newMethod)
        {
            List<GMap.NET.Point> points = new List<GMap.NET.Point>();
            GMap.NET.Size maxTileSize;
            GMap.NET.Point adjustedPoint;
            GMap.NET.Point pixelCoordinate;
            GMap.NET.Point mapPoint;
            
            maxTileSize = _projection.GetTileMatrixMaxXY(zoom);
            //Top Left Bounds
            GMap.NET.Point tlb = _projection.FromTileXYToPixel(new GMap.NET.Point(x, y));

            maxTileSize = new GMap.NET.Size(GHeat.SIZE, GHeat.SIZE);
            //Lower right bounds
            GMap.NET.Point lrb = new GMap.NET.Point((tlb.X + maxTileSize.Width) + dot.Width, (tlb.Y + maxTileSize.Height) + dot.Width);

            //pad the Top left bounds
            tlb = new GMap.NET.Point(tlb.X - dot.Width, tlb.Y - dot.Height);

       
            //Go throught the list and convert the points to pixel cooridents
            foreach (GMap.NET.PointLatLng llPoint in GetList(tlb,lrb,zoom,newMethod))
            {
                //Now go through the list and turn it into pixel points
                pixelCoordinate = _projection.FromLatLngToPixel(llPoint.Lat, llPoint.Lng, zoom);

                //Make sure the weight and data is still pointing after the conversion
                pixelCoordinate.Data = llPoint.Data;
                pixelCoordinate.Weight = llPoint.Weight;

                mapPoint = _projection.FromPixelToTileXY(pixelCoordinate);
                mapPoint.Data = pixelCoordinate.Data;

                //Adjust the point to the specific tile
                adjustedPoint = AdjustMapPixelsToTilePixels(new GMap.NET.Point(x, y), pixelCoordinate);

                //Make sure the weight and data is still pointing after the conversion
                adjustedPoint.Data = pixelCoordinate.Data;
                adjustedPoint.Weight = pixelCoordinate.Weight;

                //Add the point to the list
                points.Add(adjustedPoint);
            }

            return points.ToArray() ;
        }

        /// <summary>
        /// Gets all of the points in the list (Lat Lng Format)
        /// </summary>
        /// <returns></returns>
        public GMap.NET.PointLatLng[] GetAllPoints()
        {
            return _pointList.ToArray();
        }

        /// <summary>
        /// Gets a list of points that fit with in the Top Left Bounds and Lower Right Bounds
        /// </summary>
        /// <param name="tlb">Top Left Bounds</param>
        /// <param name="lrb">Lower Right Bounds</param>
        /// <param name="zoom"></param>
        /// <param name="newMethod"></param>
        /// <returns></returns>
        protected GMap.NET.PointLatLng[] GetList(GMap.NET.Point tlb, GMap.NET.Point lrb, int zoom, bool newMethod)
        {
            List<GMap.NET.Point> points = new List<GMap.NET.Point>();
            IEnumerable<GMap.NET.PointLatLng> llList;

            GMap.NET.PointLatLng ptlb;
            GMap.NET.PointLatLng plrb;

            ptlb = _projection.FromPixelToLatLng(tlb, zoom);
            plrb = _projection.FromPixelToLatLng(lrb, zoom);

            //Find all of the points that belong in the expanded tile
            // Some points may appear in more than one tile depending where they appear
            if (newMethod)
            {
                ListSearch ls = new ListSearch(_pointList, ptlb, plrb);
                llList = ls.GetMatchingPoints();
            }
            else
            {
                llList = from point in _pointList
                         where
                                point.Lat <= ptlb.Lat && point.Lng >= ptlb.Lng &&
                                point.Lat >= plrb.Lat && point.Lng <= plrb.Lng
                         select point;
            }
            return llList.ToArray();
        }

        /// <summary>
        /// Converts the whole map pixes to tile pixels
        /// </summary>
        /// <param name="tileXYPoint"></param>
        /// <param name="mapPixelPoint"></param>
        /// <returns></returns>
        public static GMap.NET.Point AdjustMapPixelsToTilePixels(GMap.NET.Point tileXYPoint, GMap.NET.Point mapPixelPoint)
        {
            return new GMap.NET.Point(mapPixelPoint.X - (tileXYPoint.X * GHeat.SIZE), mapPixelPoint.Y - (tileXYPoint.Y * GHeat.SIZE));
        }
    }
}
